/*
Copyright (c) Ulrich Hilger, Light Development, http://www.lightdev.com
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice, this 
list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright notice, 
this list of conditions and the following disclaimer in the documentation 
and/or other materials provided with the distribution.

- Neither the name of Light Development nor the names of its contributors may be 
used to endorse or promote products derived from this software without specific 
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package utils;

import javax.swing.text.SimpleAttributeSet;
import java.util.Hashtable;
import javax.swing.text.AttributeSet;

/**
 * A parser for XML text
 *
 * <p>Parses XML text and
 * triggers a given instance of <code>XmlParserCallback</code>.</p>
 *
 * <p>To use parsed contents in own applications a customized class that implements
 * interface <code>XmlParserCallback</code> needs to be created.</p>
 *
 * <p><b>Usage example:</b></p>
 *
 * <pre>
 * public class ExampleReader extends BufferedReader implements XmlParserCallback {
 *    public ExampleReader(Reader in) {
 *      super(in);
 *    }
 *    public void read(Object destination) throws IOException, IllegalCharacterException {
 *      XmlParser p = new XmlParser(this);
 *      String line = readLine();
 *      while(line != null) {
 *        p.parse(line);
 *        line = readLine();
 *      }
 *    }
 *    public void handleStartTag(String tagName, AttributeSet a) {
 *      System.out.println("handleStartTag, tagName='" + tagName + "'");
 *    }
 *    public void handleEndTag(String tagName) {
 *      System.out.println("handleEndTag, tagName='" + tagName + "'");
 *    }
 *    public void handleText(String text) {
 *      System.out.println("handleText, text='" + text + "'");
 *    }
 *  }
 * </pre>
 *
 * @author Ulrich Hilger
 * @author Light Development
 * @author <a href="http://www.lightdev.com">http://www.lightdev.com</a>
 * @author <a href="mailto:info@lightdev.com">info@lightdev.com</a>
 * @author published under the terms and conditions of the BSD License,
 *      for details see file license.txt in the distribution package of this software
 *
 * @version 2, February 24, 2009 (slightly adapted from version 1 from March 5, 2005 to have a single parser class with inner support classes)
 */
public class XMLParser {

  /**
   * construct a new XmlParser
   *
   * @param cb the <code>XmlParserCallback</code> this parser calls to handle tags,
   * attributes and content found
   *
   * @see XmlParserCallback
   */
  public XMLParser(XmlParserCallback cb) {
    this.cb = cb;
    initSpecialCharTable();
  }

  /**
   * parse a text portion by going through each character of
   * the data string and by driving the <code>XmlParserCallback</code>
   * according to the found data (start tags, attributes, end tags and content).
   *
   * @param data the data String to parse
   *
   * @see XmlParserCallback
   */
  public void parse(String data) throws IllegalCharacterException {
    //System.out.println("parse data='" + data + "'");
    for (int i = 0; i < data.length(); i++) {
      char curChar = data.charAt(i);
      switch (curChar) {
        case tagStart:
          startTag();
          break;
        case tagEnd:
          if (inTag) {
            endTag();
          } else {
            text(curChar);
          }
          break;
        case endTagStart:
          if (!isDocType && !inAttrValue) {
            if (inTag) {
              tagEndStart();
            } else {
              text(curChar);
            }
          } else {
            text(curChar);
          }
          break;
        case commentEnd:
          if (inTag && lastChar == commentStart) {
            comment();
          } else {
            text(curChar);
          }
          break;
        case attrStart:
          //System.out.println("parse attrStart (blank) found");
          if (!isDocType && inTag && !isEndTag && !inAttribute) {
            //System.out.println("parse calling startAttribute");
            startAttribute();
          } else {
            //System.out.println("parse calling text, curChar='" + curChar + "'");
            text(curChar);
          }

          break;
        case attrNameEnd:
          if (inTag && !isEndTag && inAttribute) {
            endAttrName();
          } else {
            text(curChar);
          }
          break;

        case attrValueBorder:
          processAttrValueBorder(curChar);
          /*if (!isDocType && inTag && !isEndTag) {
          if (inAttrValue) {
          endAttrValue();
          }
          else {
          startAttrValue();
          }
          }
          else {
          text(curChar);
          }*/
          break;

        case altAttrValueBorder:
          processAttrValueBorder(curChar);
          /*if (!isDocType && inTag && !isEndTag) {
          if (inAttrValue) {
          endAttrValue();
          }
          else {
          startAttrValue();
          }
          }
          else {
          text(curChar);
          }*/
          break;

        case special:
          if (inSpecial) {
            // ignore: special character start (&) detected while already inside special character
          } else {
            openSpecialChar();
          }
          break;
        case specialEnd:
          if (inSpecial) {
            closeSpecialChar();
          } else {
            text(curChar);
          }
          break;
        default:
          text(curChar);
          break;
      }
      lastChar = curChar;
    }
  }

  private void processAttrValueBorder(char curChar) {
    if (!isDocType && inTag && !isEndTag) {
      if (inAttrValue) {
        endAttrValue();
      } else {
        startAttrValue();
      }
    } else {
      text(curChar);
    }
  }

  /**
   * start writing parsed characters to the buffer for special characters
   */
  private void openSpecialChar() {
    inSpecial = true;
    specialBuf = new StringBuffer();
  }

  /**
   * end writing parsed characters to the buffer for special characters and
   * transform the found special character notation to the character it designates
   */
  private void closeSpecialChar() {
    buf.append(specialChars.get(specialBuf.toString()));
    inSpecial = false;
  }

  /**
   * initialize the table that maps special character notations to special characters, e.g.
   * lt = <, gt = >, amp = &, etc.
   */
  private void initSpecialCharTable() {
    specialChars = new Hashtable();
    specialChars.put("lt", "<");
    specialChars.put("gt", ">");
    specialChars.put("amp", "&");
    specialChars.put("quot", "\"");
    specialChars.put("apos", "'");
  }

  /**
   * process the start of a tag ('<'), i.e. send the previously parsed
   * data to the callback class and set indicators accordingly
   *
   * @throws IllegalCharacterException, if a '<' character was in the content
   */
  private void startTag() throws IllegalCharacterException {
    //System.out.println("XmlParser.startTag tagName=" + tagName);
    if (!inTag) {
      flushBuffer();
      isEndTag = false;
      inTag = true;
      tagName = null; // important to reset tagName here, otherwise startAttribute will not save the tag name later
    } else {
      throw new IllegalCharacterException("'" + tagStart + "' is not legal inside content");
    }
  }

  /**
   * process the start of an attribute (' '), i.e.
   * save the parse buffer to be the name of the currently parsed tag,
   * reset the parse buffer and indicate, taht we are inside an attribute
   * now
   */
  private void startAttribute() {
    //System.out.println("startAttribute (entering) tagName=" + tagName + ", buf=" + buf);
    if (tagName == null) {
      tagName = buf.toString().trim();
      if (tagName.equalsIgnoreCase("!DOCTYPE")) {
        isDocType = true;
      }
      buf = new StringBuffer();
    }
    inAttribute = true;
  //System.out.println("startAttribute (leaving) tagName=" + tagName + ", buf=" + buf);
  }

  /**
   * process the border of an attribute value ('"') in cases
   * where it starts an attribute value, i.e.
   * set the indicator to the parser being inside an attribute value
   */
  private void startAttrValue() {
    //System.out.println("startAttrValue tagName=" + tagName + ", buf='" + buf + "'");
    inAttrValue = true;
  /*attrName = buf.toString();
  buf = new StringBuffer();*/
  }

  /**
   * process the end of an attribute name ('='), i.e.
   * save the parse buffer as the attribute name and reset
   * the parse buffer
   */
  private void endAttrName() {
    //System.out.println("endAttrName tagName=" + tagName + ", buf=" + buf);
    attrName = buf.toString().trim();
    buf = new StringBuffer();
  }

  /**
   * process the end of an attribute value (';'), i.e.
   * add the attribute name and value to the set of attributes
   * for the current tag and reset parse buffer and indicators
   */
  private void endAttrValue() {
    //System.out.println("XmlParser.endAttrValue, attrName='" + attrName + "', attrValue='" + buf.toString() + "'");
    a.addAttribute(attrName, buf.toString());
    buf = new StringBuffer();
    inAttribute = false;
    inAttrValue = false;
  }

  /**
   * process an end tag, i.e. save the tag name
   * when no attributes were found and call the callback class,
   * then reset indicators
   */
  private void endTag() {
    //System.out.println("XmlParser.endTag (entering) tagName=" + tagName);
    if (a.getAttributeCount() < 1) {
      //System.out.println("XmlParser.endTag, attr count=" + a.getAttributeCount() + ", tagName=" + buf.toString());
      tagName = buf.toString();
    }
    flushBuffer();
    isEndTag = false;
    inTag = false;
    inComment = false;
    isQTag = false;
    inAttribute = false;
  //System.out.println("XmlParser.endTag (leaving) tagName=" + tagName);
  }

  /**
   * process the start of an end tag ('/'), i.e.
   * set the isEndTag indicator
   */
  private void tagEndStart() {
    isEndTag = true;
  }

  /**
   * give the buffer of parsed data back to the <code>XmlParserCallback</code> and
   * reset the parse buffer. This calls the appropriate interface method depending on
   * the type of data that has been parsed (start tag, end tag or content).
   *
   * @see XmlParserCallback
   */
  private void flushBuffer() {
    //System.out.println("XmlParser.flushBuffer inTag=" + inTag + ", isEndTag=" + isEndTag + ", tagName=" + tagName);
    if (isDocType) {
      cb.handleStartTag("!DOCTYPE " + tagName.trim() + " ", a);
      //cb.handleText(buf.toString());
      cb.handleEndTag(tagName);
      isDocType = false;
    } else if (buf != null && !inComment && !isQTag) {
      if (inTag) {
        if (isEndTag) {
          if (lastChar == endTagStart) {
            cb.handleStartTag(tagName, a);
          //a = new SimpleAttributeSet();
          }
          cb.handleEndTag(buf.toString());
        } else {
          cb.handleStartTag(tagName, a);
        /*if(isDocType) {
        cb.handleEndTag(buf.toString());
        isDocType = false;
        }*/
        //a = new SimpleAttributeSet();
        }
      } else {
        cb.handleText(buf.toString()/*.trim()*/);
      }
    }
    buf = new StringBuffer();
    a = new SimpleAttributeSet();
  }

  /**
   * process content text, i.e. store it in the parse buffer
   *
   * @param c the character to treat as content text
   */
  private void text(char c) {
    if (inSpecial) {
      specialBuf.append(c);
    } else if (!inComment && !isQTag) {
      buf.append(c);
    //System.out.println("text appended c='" + c + "', buf='" + buf.toString() + "'");
    }
  }

  /**
   * process a comment tag, i.e. set the comment indicator
   */
  private void comment() {
    inComment = true;
  }

  public class IllegalCharacterException extends Exception {

    /**
     * constructor
     * @param message the message to show along with the exception
     */
    public IllegalCharacterException(String message) {
      super(message);
    }
  }

  public interface XmlParserCallback {

    /**
     * this method is called by <code>XmlParser</code> when a start tag is
     * encountered in the data that is being parsed.
     *
     * @param tagName the name of the tag that has been encountered
     * @param a the attributes of the tag that has been encountered
     * @see XMLParser
     */
    public void handleStartTag(String tagName, AttributeSet a);

    /**
     * this method is called by <code>XmlParser</code> when an end tag is
     * encountered in the data that is being parsed.
     *
     * @param tagName the name of the tag that has been encountered
     * @see XMLParser
     */
    public void handleEndTag(String tagName);

    /**
     * this method is called by <code>XmlParser</code> when a portion
     * of content text has been parsed.
     *
     * @param text the text portion that has been parsed
     * @see XMLParser
     */
    public void handleText(String text);
  }
  /* --------------- class fields start ------------------------ */
  /**
   * the <code>XmlParserCallback</code> object to handle parsed tags and content
   *
   * @see XmlParserCallback
   */
  private XmlParserCallback cb;
  /**
   * an attribute set to hold encountered tag attributes until
   * they are sent to the <code>XmlParserCallback</code> object for handling
   *
   * @see XmlParserCallback
   */
  private SimpleAttributeSet a = new SimpleAttributeSet();
  /**
   * a buffer to hold parsed data such as tag names, attribute names,
   * attribute values or content text until
   * it is sent to the <code>XmlParserCallback</code> object for handling
   *
   * @see XmlParserCallback
   */
  private StringBuffer buf = new StringBuffer();
  /** buffer for special characters */
  private StringBuffer specialBuf;
  /**
   * table that maps special character notations totheir corresponding special character, e.g.
   * lt = <, gt = >, amp = &, etc.
   */
  private Hashtable specialChars;
  /** storage for the most recently parsed character */
  private char lastChar;
  /**
   * temporary storage to save a tag name before
   * the buffer gets used for something else
   */
  private String tagName;
  /**
   * temporary storage to save an attribute name before
   * the buffer gets used for something else
   */
  private String attrName;
  /** indicates whether or not the parser currently is inside a DOCTYPE tag */
  private boolean isDocType = false;
  /** indicates whether or not the parser currently is inside a tag */
  private boolean inTag = false;
  /** indicates whether or not the parser currently is inside an attribute */
  private boolean inAttribute = false;
  /** indicates whether or not the parser currently is inside an attribute value */
  private boolean inAttrValue = false;
  /** indicates whether or not the parser currently is inside an end tag */
  private boolean isEndTag = false;
  /** indicates whether or not the parser currently is inside a comment tag */
  private boolean inComment = false;
  /** inidicates whether or not the parser currently is inside a special character */
  private boolean inSpecial = false;
  /** indicates whether or not the parser currently is inside a 'question mark' tag */
  private boolean isQTag = false;
  /** character indicating the start of a tag */
  public static final char tagStart = '<';
  /** character indicating the end of a tag */
  public static final char tagEnd = '>';
  /** character indicating the start of an end tag */
  public static final char endTagStart = '/';
  /** question mark character */
  public static final char qChar = '?';
  /** exclamation character */
  public static final char exChar = '!';
  /** character indicating the start of an attribute */
  public static final char attrStart = ' ';
  /** character indicating the start of an attribute value */
  public static final char attrValueBorder = '"';
  public static final char altAttrValueBorder = '\'';
  /** character indicating the start of an attribute value */
  public static final char attrNameEnd = '=';
  /** character indicating the end of a special character */
  public static final char specialEnd = ';';
  /** character indicating the start of a comment */
  public static final char commentStart = '!';
  /** character indicating the end of a comment */
  public static final char commentEnd = '-';
  /** character indicating special characters */
  public static final char special = '&';

  /* ------------------ class fields end ---------------------- */
}
